如何设计和实现一个web app

web app简介

web app其实不算是什么新鲜的东西,相比于传统的web和传统的app,web app这种web和app相结合的产物有的优点如下:

  1. 开发上web app更有便捷性,ios开发一上来需要安装一堆东西,android开发也差不多,另外web app的学习成本要比平台客户端开发要低些,至少你不用去招聘ios和android程序员。只要具备基础web开发能力的人都可以比较快上手。

  2. 部署方便,在很多情况下,部署一个单页web app只需要一个index.html页面文件作为容器和一个前端资源打包文件(一般叫bundle.js)即可,然后把index.html放在自己服务器项目路径下,在其中引入bundle.js,至于bundle.js则可以放在CDN上,更新web app就等于是上传并刷新CDN缓存。这样一来前端部署极其简单,基本上不需要SSH到服务器去更换文件,也可以避免自己的服务器传输比较耗费带宽的前端资源文件。

  3. 单页web app如果技术选型得当,开发和维护成本都相对比较低。

  4. 可以适应更多环境。

凡事都有利弊,web app也有不尽如人意的地方,web app的主要缺点如下:

  1. 性能上不如原生app,这个不用多说,基本是不可改变的事实。

  2. 暂时还没想到其他的。

前期准备

首先要进行技术选型,根据作者的经历,我选择的是react+flux,flux是一种数据流的架构方式,严格来说它并不是某一种特定的实现。比较常用的实现有facebook官方的flux实现,还有一些第三方实现(比如redux)。要注意的是,flux的具体实现基本不会影响到我们的项目开发,具体选择哪个实现就看你的喜好了(当然最好不要中途再做改变:D)。

那么就要来简单介绍一下react是什么,react可以说是一个改变前端生态系统的发明,传统web开发中,我们以页面为单位来设计模块,如果有多个页面都用到了某个功能,比如一个列表,那么就要重复地写HTML和JS实现它,代码抽象程度很低,冗余度很高,也不利于后期维护。react推出了UI组件的概念,让web前端开发人员可以封装UI组件用于复用。

flux这种数据流架构思想其实也不是什么新东西,但是真正在web前端大规模使用也是在开发人员使用react+flux之后的事情。flux制定了一些web前端数据获取和分发的规则,虽然刚开始看上去比较复杂,但是一旦你理解了它的思想,其实很简单,而且对于维护一个web app来说,react+flux可以说是相当不错的组合。

设计一个web app具体是在设计什么?

刚才谈到了react+flux,其实UI组件的封装和数据流的架构,它们都帮开发人员规定好了,我们需要做的就是去好好运用它,那么还有什么可设计的呢?

还是有的,而且不少。从技术角度来说,web app和ios app、android app这些原生app在技术设计上有很多共通之处:

  1. 页面生命周期管理
  2. 公共函数的设计
  3. 局部/全局事件和通知
  4. UI组件API设计
  5. 常量定义
  6. 网络请求

web app也具备一些原生app所没有或不一样的特性,例如app路由、JS(ES5/ES6差异)、使用/刷新CDN缓存、前端资源打包、快速部署等。

react+flux是怎么工作的

由于本篇文章并不是react和flux的教程,所以只会大致介绍一下它们的工作方式。

下面是官方给出的一个flux工作流程图:

Flux工作流程图

详细版:

Flux工作流程图-详细版

简单解释一下flux的工作流程:

在第一张图中,有两种流程:

  1. Action->Dispatcher->Store->View

  2. View->Action->Dispatcher->Store->View

在Action->Dispatcher->Store->View中,可以通过调用Action中的方法来执行一项操作,这个操作可以是向服务器请求数据,或仅仅在本地改变数据,操作完成后,Action会通知Dispatcher,然后由后者来分派动作和数据。在Store中,事先注册好对各个类型事件的回调函数,当Store接收到Dispatcher分发来的事件和数据时,就执行一些更新操作。另外View可以对Store注册监听器,一但Store中的数据有变化,会立即执行View预先设置好的监听器回调函数,这一般会是一个更新View的操作,这相当于一个发布-订阅模式。

在View->Action->Dispatcher->Store->View中,实际上是说指用户和View之间的交互导致数据变更(不管是请求服务器还是本地数据改变),其他操作和Action->Dispatcher->Store->View基本一样。

光看这些是十分抽象的,如果没有深入去看一个demo或者自己实现一个项目,确实有点不好理解。我将在后续的文章中介绍react和flux –> react+flux编程实践(一) 基础篇,并实际剖析一个官方demo:todo-mvc –> react+flux编程实践(二) 实践篇

本篇中react+flux基本介绍到这。下面要谈谈具体设计。

具体设计

一个web app无非就是颠来倒去地做以下几件事:

  1. 调用网络API
  2. 展示页面
  3. 数据本地存储(这里一般指非持久化的那种,这和原生app有所不同)
  4. 接受用户输入并反馈

作为技术人员,我们首先要明确几点:

  1. 明确地知道业务放需要什么。
  2. 划分功能模块。
  3. 弄清楚各个问题之间的依赖关系,制定模块之间的通信规范。
  4. 适当考虑项目未来走向,对架构设计留有余地。
  5. 分析风险。
  6. 考虑如何解决依赖关系中最基础的部分,先实现基础模块(或者至少你要先对此有个大致的设计),不断在此基础上完善整个架构。这部分开发人员会花费比较大的精力去做,因为这影响到整个项目未来的几乎所有事情。同时在这个过程中不断审视架构是否合理,及时调整。
  7. 单元测试,性能测试(如果项目需要且有时间的话)。

项目文件结构

好的开发习惯其中一个就是要制定清晰的项目文件结构,并且从始至终保持下去。

适当的文档描述

如果可能的话,适当写一些帮助性的架构文档,用简洁明确的词语传达你想要表达的,让后来的程序员可以快速上手。

思路和方法同一,不搞多元化

在一个项目中,对同一类事情应该有一个统一的处理方法,包括代码风格、变量和方法的命名规范、调用规范、流程规范等,事先制定出来,并且要求所有人都要遵守。

尽量少的横向依赖,尽量减少跨层访问,降低模块间耦合度

这部分内容react+flux已经帮我们做了很大一部分。

对业务方该限制的地方有明确的限制,该灵活的地方要给业务方创造灵活实现的条件

可以通过良好的设计来保证这一点。

接下来会对划分功能模块、模块间通信规范、解决依赖关系这几个方面进行说明。

功能模块的划分

在react+flux的项目中,不谈具体业务的情况下,有几个大的基础模块是一定要考虑的:

  1. 展示模块
  2. 网络请求模块
  3. 本地存储模块
  4. 路由模块
  5. 公用模块

1. 展示模块

这是用户能直接看到的东西,我们用react封装各个UI组件提供出来,也可以引入第三方UI组件,这本身已经是一种进步,让我们可以在web前端“面向组件编程”,因此对UI组件的处理就尤为关键。ES6语法下,我们可以用extends react.Component来建立一个UI组件,然后在组件“类”中写初始化方法、渲染方法、生命周期函数、事件回调等方法,然后把它作为一个整体提供出去,这里就涉及到UI组件API的设计,UI组件可以接受属性值,这些属性应该尽可能的少和命名清晰、简单,保持简洁性很有必要。另外组件的展示离不开CSS和一些资源文件,作为一种封装,把CSS、图片等资源文件一并放在这个UI组件的文件夹下也是很有必要的。

2. 网络请求模块

web app网络请求全靠ajax,在和服务器交互时,应该和服务端约定好返回数据的格式,如错误代码的含义、出错信息、详细的数据格式等,并且很有必要在web app端做一层封装,比如封装出一个request模块,在服务器返回数据后首先解析返回数据,如果出错就报错,成功就执行用户回调函数等。这部分作为整个项目对外的接口应该考虑到所有可能的网络情况:服务器致命错误、普通错误、成功、超时、无网络等。

3. 本地存储模块

这部分主要交给flux处理,我们需要做的就是设计本地数据的结构,使其尽量合理并适应多种应用场景。

4.路由模块

react+flux的单页web app一般采用react-router作为路由,这可以省下你不少时间和精力,react-router是一个非常成熟和优秀的路由模块。

5. 公共模块

公共模块一般包括但不仅限于:公用函数、helper、常量定义、应用级别的功能(如展示loading框、警告框、确认框这些)。这部分功能也很重要,比如loading框,我们在发起网络请求时会展示它,数据返回时它会消失,那么这个框的出现就与上面说到的网络模块有关系,警告框和确认框也是常用的东西,这些组件都可应该在app层面上进行设计和整合,而不应该放在各个UI组件内部,因为这个是一个app中的通用功能。

模块间通信规范

在react+flux中,使用一种单向数据流的方式来分发数据,这就让整个数据走向非常清楚,我们的web app模块间通信规范就是根据这个单向数据流的思想来的。

解决依赖关系

react+flux中处理依赖关系时,用的比较多的方式无非3种:显式引入、基于事件(发布-订阅模式)、回调函数。

在要向模块中引入其他模块时(import),使用显式引入,这种依赖关系最强也最明显。如果是依赖关系没有那么强,可以考虑用后面两种,这有助于代码的简介和模块解耦。

基于事件(发布-订阅模式),举个简单的例子,flux中当Store中的数据变化时,要通知相对应的View更新页面,典型的处理方式是让Store成为一个EventEmitter,同时View对该Store addChangeListener,即成为它的订阅者,当Store改变时会自动调用该View的监听回调,让View更新界面。

基于回调函数,有一个非常典型的用例,在设计通用的警告框和确认框组件时,有时我们需要在用户点击“确定”和“取消”按钮时做一些事情,当然也可能什么也不做。这时候我们不要忘了JS中函数是一等公民这件事,我们可以把事件处理函数传递给警告框和确认框组件,这样就很巧妙的解决了跨组件、跨模块通信而不会使模块过于耦合了。